
from dice import *
import logging, random

"""
Specifies several types of weapons, and a base class for defining other weapons.
"""

"""
Common abbreviations:
-cmd: Critical Miss Damage
-nhd: Normal Hit Damage
-chd: Critical Hit Damage
-dlpi: Distance Lost Per Inch (only for ranged weapons)
"""

class Weapon(object):
    """
    Defines the basic interface for a weapon. Animals can 
    have weapons, too; their mouth.
    """
    
    logger = logging.getLogger('engine.Weapon')
    owner = None
    
    toHitDice = None
    toHitMin = 2
    criticalHitMin = 3
    criticalMissMax = 1
    
    dropable = True
    
    def __init__(self, holder):
        self.owner = holder
    
    def fire(self, target, obscured, distanceToTarget):
        """
        Used to actually fire a weapon. Return value is not significant.
        """
        raise NotImplementedError
    
    def getDamage(self, obscured, distanceToTarget):
        """
        Returns two values:
        -A boolean indicating that it hit the target (vs. its owner, default True)
        -A number indicating damage (0 if missed)
        """
        raise NotImplementedError
    
    def parry(self, attacker, parryBonus=0):
        """
        Used to block some weaponry, mostly cutlasses. A certain amount
        of associated logic will need to be in the owner. Returns whether
        or not the weapon parries this turn.
        """
        raise NotImplementedError

class RangedWeapon(Weapon):
    """
    For pistols, rifles, muskets, bow & arrow, ... 
    Assumes all ranged weapons are disabled when they parry successfully.
    """
    logger = logging.getLogger('engine.RangedWeapon')
    operable = True
    
    def fire(self, target, obscured, distanceToTarget):
        self.logger.debug("Firing weapon on %s at %s." % (self.owner, target))
        dieRoll = self.toHitDice.rollSum()
        hitTarget, damage = self.getDamage(obscured, distanceToTarget)
        if hitTarget and damage > 0:
            self.logger.debug("fire: Hitting target for %s damage" % damage)
            target.takeDamage(damage)
        elif not hitTarget and damage > 0:
            self.logger.debug("fire: Hitting owner for %s damage" % damage)
            self.owner.takeDamage(damage)
    
    def getDamage(self, obscured, distanceToTarget):
        if not self.operable: return True, 0
        
        toHit = target.toHitRoll
        dieRoll = self.toHitDice.rollSum()
        self.logger.debug('getDamage: To-hit roll (raw): %s' % dieRoll)
        
        if obscured:
            dieRoll -= self.obscuredPenalty
            self.logger.debug('  With obscured target penalty: %s' % dieRoll)
        
        if distanceToTarget > self.range:
            dieRoll -= (distanceToTarget - self.range) * self.accuracyLostPerInch
        
        if dieRoll < self.criticalMiss:
            hitTarget, damage = False, self.cmd
            self.logger.debug('Critical miss.')
        elif toHit <= dieRoll < self.criticalHit:
            hitTarget, damage = True, self.nhd
            self.logger.debug('Normal hit.')
        elif dieRoll >= self.criticalHit:
            hitTarget, damage = True, self.chd
            self.logger.debug('Critical hit.')
        else:
            hitTarget, damage = True, 0
            self.logger.debug('Normal miss.')
        
        return hitTarget, damage
    
    def parry(self, attacker, parryBonus=0):
        if self.toParryDice.roll()+parryBonus > self.parryOn:
            #We manage to parry
            self.operable = False
            return True
        else:
            return False

class Pistol(RangedWeapon):
    logger = logging.getLogger('engine.Pistol')
    toHitDice = Dice('2d6')
    criticalMiss = 3
    criticalHit = 12
    obscuredPenalty = 1
    range = 6
    accuracyLostPerInch = 2
    cmd = 1
    nhd = 2
    chd = 4
    toParryDice = Dice('2d9')
    parryOn = 11
    operable = True

class Rifle(RangedWeapon):
    logger = logging.getLogger('engine.Rifle')
    toHitDice = Dice('2d9')
    criticalMiss = 3
    criticalHit = 17
    obscuredPenalty = 2
    range = 8
    accuracyLostPerInch = .5
    cmd = 2
    nhd = 3
    chd = 6
    toParryDice = Dice('2d9')
    parryOn = 7
    operable = True

class Cannon(RangedWeapon):
    """
    A regular cannon that aims at the hull of a ship. Other variations to come 
    later (maybe).
    """
    logger = logging.getLogger('engine.Cannon')
    toHitDice = Dice('2d4')
    criticalMiss = 3
    criticalHit = 8
    obscuredPenalty = 0
    range = 18
    accuracyLostPerInch = .125
    cmd = 0
    nhd = 10
    chd = 20
    def parry(self):
        return False

class MeleeWeapon(Weapon):
    """
    For contact weapons like cutlasses, clubs, oars, shovels, ...
    """
    def fire(self, target, obscured, distanceToTarget):
        self.logger.debug("Firing weapon %s at %s" % (self, target))
        if distanceToTarget > self.range:
            self.logger.debug("  Too far to target. Failing.")
            return
        
        toHit = target.toHitRoll
        tohitRoll = self.toHitDice.rollSum()
        if obscured:
           tohitRoll -= self.obscuredPenalty
        
        if tohitRoll < self.criticalMiss:
            self.takeDamage(self.cmd)
            return
        elif self.toHit <= tohitRoll < self.criticalHit:
            damage = random.randint(self.damageMin, self.damageMax)
        elif self.criticalHit <= tohitRoll:
            damage = 2*random.randint(self.damageMin, self.damageMax)
        
        if not target.parry(self.owner, self, damage):
            target.takeDamage(damage)
        
    def parry(self, attacker, parryBonus=0):
        return (self.toParryDice.roll() + parryBonus  >  self.parryOn)

class Cutlass(MeleeWeapon):
    logger = logging.getLogger('engine.Cutlass')
    toHitDice = Dice('2d9')
    criticalMiss = 3
    toHit = 11
    criticalHit = 17
    range = 1
    cmd = 1
    damageMin = 1
    damageMax = 5
    toParryDice = Dice('2d9')
    parryOn = 10
    obscuredPenalty = 2
    